/*
 * Copyright (c) 2013-2018 上海汇数数据. All rights reserved.
 * @(#) Hmac.java 2018-08-09 23:05
 */

package cn.jh.common.core.crypto;

import cn.jh.common.core.base.Base64Kt;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.jetbrains.annotations.NotNull;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.List;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;


/**
 * MAC 哈希算法工具（不支持，也不推荐自己实现 {@code HmacMD5}）。
 * <p/>
 * SunJCE provider Mac: {@code HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512}
 * <table>
 * <capture>KeyGenerator (No key size restriction)</capture>
 * <tr><th>Algorithm Name</th><th>Default KeySize</th></tr>
 * <tr><td>HmacMD5</td><td>512</td></tr>
 * <tr><td>HmacSHA1</td><td>512</td></tr>
 * <tr><td>HmacSHA256</td><td>256</td></tr>
 * <tr><td>HmacSHA384</td><td>384</td></tr>
 * <tr><td>HmacSHA512</td><td>512</td></tr>
 * </table>
 *
 * @author Fuchun
 * @since 1.0
 */
public class Hmac {

    private static final String HMAC_SHA1 = "HmacSHA1";
    private static final String HMAC_SHA256 = "HmacSHA256";
    private static final String HMAC_SHA384 = "HmacSHA384";
    private static final String HMAC_SHA512 = "HmacSHA512";
    private static final ImmutableList<String> SUPPORTS_ALGORITHM = ImmutableList.of(
            HMAC_SHA1, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512
    );

    private static final Map<String, Hmac> HMAC_MAP;

    static {
        ImmutableMap.Builder<String, Hmac> builder = ImmutableMap.builder();
        builder.put(HMAC_SHA1, new Hmac(HMAC_SHA1));
        builder.put(HMAC_SHA256, new Hmac(HMAC_SHA256));
        builder.put(HMAC_SHA384, new Hmac(HMAC_SHA384));
        builder.put(HMAC_SHA512, new Hmac(HMAC_SHA512));

        HMAC_MAP = builder.build();
    }

    @NotNull
    public static Hmac sha1() {
        return HMAC_MAP.get(HMAC_SHA1);
    }

    @NotNull
    public static Hmac sha256() {
        return HMAC_MAP.get(HMAC_SHA256);
    }

    @NotNull
    public static Hmac sha384() {
        return HMAC_MAP.get(HMAC_SHA384);
    }

    @NotNull
    public static Hmac sha512() {
        return HMAC_MAP.get(HMAC_SHA512);
    }

    private final String algorithm;

    private Hmac(String algorithm) {
        checkArgument(SUPPORTS_ALGORITHM.contains(algorithm),
                "No such algorithm `%s` for Hmac.", algorithm);
        this.algorithm = algorithm;
    }

    public String getAlgorithm() {
        return algorithm;
    }

    @NotNull
    private Mac getMac(String key) throws IllegalArgumentException {
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(Charsets.UTF_8), algorithm);
        return getMac(keySpec);
    }

    @NotNull
    private Mac getMac(SecretKeySpec keySpec) throws IllegalArgumentException {
        try {
            Mac mac = Mac.getInstance(algorithm);
            mac.init(keySpec);

            return mac;
        } catch (NoSuchAlgorithmException ex) {
            // never happen
            throw new IllegalArgumentException("No such algorithm `" + algorithm + "` for Mac.");
        } catch (InvalidKeyException ex) {
            throw new IllegalArgumentException("Invalid key: `" + Base64Kt.encode(keySpec.getEncoded()) + "`");
        }
    }

    /**
     * 生成一个随机的与当前指定算法匹配的密钥。
     */
    public Key genKey() {
        try {
            KeyGenerator keyGen = KeyGenerator.getInstance(algorithm, "SunJCE");

            return keyGen.generateKey();
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalArgumentException(ex);
        } catch (NoSuchProviderException ex) {
            throw new InternalError(ex);
        }
    }

    @NotNull
    public byte[] hash(byte[] data, String key) {
        checkArgument(data != null && data.length > 0,
                "The data must be not null or empty.");
        Mac mac = getMac(key);
        return mac.doFinal(data);
    }

    @NotNull
    public byte[] hash(String data, String key) {
        checkArgument(data != null && !data.isEmpty(),
                "The data must be not null or empty.");
        return hash(data.getBytes(Charsets.UTF_8), key);
    }

    @Override
    public String toString() {
        return String.format("Hmac{algorithm=%s}", algorithm);
    }
}
